﻿namespace Home.Security
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Security.Cryptography.X509Certificates;
    using System.Security.Cryptography.Xml;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web.Security;
    using System.Xml;
    using System.Xml.Linq;
    using Newtonsoft.Json;
    using JsonFormatting = Newtonsoft.Json.Formatting;

    public static class SecurityHelper
    {
        public const string DefaultAuthenticationType = "ApplicationCookie";
        public const string IdentityProviderClaimType = "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider";
        public const string AntiForgeryAjaxFieldName = "__AjaxRequestVerificationToken";
        public const string AntiForgeryAjaxHeaderName = "RequestVerificationToken";
        public const string AuthenticationKeyClaimType = "urn:home:authentication:identity";

        public static X509Certificate2 FindCertificate(StoreLocation location, StoreName name, X509FindType type, object value)
        {
            Debug.Assert(Enum.IsDefined(typeof(StoreLocation), location));
            Debug.Assert(Enum.IsDefined(typeof(StoreName), name));
            Debug.Assert(Enum.IsDefined(typeof(X509FindType), type));
            Debug.Assert(value != null);

            using (var store = new X509Store(name, location))
            {
                store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
                var certficates = store.Certificates.Find(type, value, false);

                if (certficates == null || certficates.Count == 0)
                {
                    return null;
                }

                if (certficates.Count > 1)
                {
                    throw new InvalidOperationException("Multiple certificate be found.");
                }

                return certficates[0];
            }
        }

        public static string NewRequestId(X509Certificate2 certificate)
        {
            Debug.Assert(certificate != null);

            var id = string.Concat(DateTime.UtcNow.ToString("yyyyMMdd", CultureInfo.InvariantCulture), Guid.NewGuid().ToString("N"));
            return ProtectData(id, certificate);
        }

        public static bool TryParseRequestId(string id, out DateTime time, out Guid guid, out string value)
        {
            time = default(DateTime);
            guid = default(Guid);
            value = default(string);

            if (string.IsNullOrEmpty(id))
            {
                return false;
            }

            var xml = default(XmlDocument);
            try
            {
                var data = UnprotectData(id);
                xml = new XmlDocument();
                xml.LoadXml(data);
            }
            catch
            {
                return false;
                throw;
            }

            if (xml.FirstChild.Name != "data")
            {
                return false;
            }

            value = xml.FirstChild.InnerText;
            if (value == null || value.Length != 40)
            {
                return false;
            }

            var dateString = value.Substring(0, 8);
            if (!DateTime.TryParseExact(dateString, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out time))
            {
                return false;
            }

            var guidString = value.Substring(8);
            if (!Guid.TryParse(guidString, out guid))
            {
                return false;
            }

            return true;
        }

        public static string ConvertToRawRequestId(string id)
        {
            DateTime time;
            Guid guid;
            string raw;
            if (!TryParseRequestId(id, out time, out guid, out raw))
            {
                throw new InvalidOperationException(string.Format("The request ID {0} is invalid.", id));
            }

            return raw;
        }

        public static string ProtectData(string data, X509Certificate2 certificate)
        {
            Debug.Assert(data != null);
            Debug.Assert(certificate != null);

            var xml = new XmlDocument();
            var element = xml.CreateElement("data");
            element.InnerText = data;
            xml.AppendChild(element);

            var eXml = new EncryptedXml();
            var edElement = eXml.Encrypt(element, certificate);
            EncryptedXml.ReplaceElement(element, edElement, false);

            return xml.OuterXml;
        }

        public static string UnprotectData(string data)
        {
            Debug.Assert(data != null);

            var xml = new XmlDocument();
            xml.PreserveWhitespace = true;
            xml.LoadXml(data);

            EncryptedXml eXml = new EncryptedXml(xml);
            eXml.DecryptDocument();

            return xml.OuterXml;
        }

        public static string ProtectData(object data)
        {
            Debug.Assert(data != null);

            var json = JsonConvert.SerializeObject(data, JsonFormatting.None);
            var jsonBytes = Encoding.UTF8.GetBytes(json);
            var protectedBytes = MachineKey.Protect(jsonBytes, "Home", "Diamond");
            var result = Convert.ToBase64String(protectedBytes, Base64FormattingOptions.None);
            return result;
        }

        public static T UnprotectData<T>(string data)
        {
            Debug.Assert(data != null);

            var base64Bytes = Convert.FromBase64String(data);
            var unprotectedBytes = MachineKey.Unprotect(base64Bytes, "Home", "Diamond");
            var json = Encoding.UTF8.GetString(unprotectedBytes);
            var result = JsonConvert.DeserializeObject<T>(json);
            return result;
        }

        internal static IDictionary<string, string> ToEntityProperties(this string propertiesString)
        {
            var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            if (!string.IsNullOrEmpty(propertiesString))
            {
                var xmlDoc = XDocument.Parse(propertiesString, LoadOptions.None);
                foreach (var xmlElementProperty in xmlDoc.Root.Elements("property"))
                {
                    var key = xmlElementProperty.Attribute("name").Value;
                    var value = xmlElementProperty.Value;
                    result.Add(key, value);
                }
            }

            return result;
        }

        internal static string ToRepositoryProperties(this IDictionary<string, string> properties)
        {
            var xmlDoc = new XDocument(new XElement("properties"));

            if (properties != null)
            {
                foreach (var item in properties)
                {
                    var xmlElementProperty = new XElement("property", item.Value);
                    xmlElementProperty.Add(new XAttribute("name", item.Key));
                    xmlDoc.Root.Add(xmlElementProperty);
                }
            }

            return xmlDoc.ToString(SaveOptions.DisableFormatting);
        }
    }
}
